// --------------------------------------------------------------------------------------------------------------------
// <copyright file="StepBase.cs" company="">
//   
// </copyright>
// --------------------------------------------------------------------------------------------------------------------
namespace ExecutionTool.Common
{
    using System;
    using System.Timers;

    using ExecutionTool.Profile;

    using Microsoft.Practices.EnterpriseLibrary.Logging;
    using Microsoft.Practices.ServiceLocation;

    public abstract class StepBase<TStepInfo> : IStep<TStepInfo>
    {
        #region Constants

        private const double ProgressHeartbeatDefaultInterval = 15 * 1000;

        private const int ProgressHeartbeatUnexpectedExceptionCountLimit = 5;

        #endregion

        #region Fields

        protected readonly LogWriter Logger;

        protected readonly Profile Profile;

        protected readonly Timer ProgressHeartbeatTimer;

        // defaut to 15 seconds interval
        private readonly bool timerBasedProgressCheck;

        private double progress;

        private int progressHeartbeatUnexpectedExceptionCount;

        #endregion

        #region Constructors and Destructors

        protected StepBase(Profile profile, bool timerBasedProgressCheck, LogWriter logger, double? progressHeartbeatInterval = null)
        {
            Helper.CheckForNull(profile, "profile");
            this.Profile = profile;

            this.Logger = logger ?? ServiceLocator.Current.GetInstance<LogWriter>();

            if (timerBasedProgressCheck)
            {
                this.ProgressHeartbeatTimer =
                    new Timer(progressHeartbeatInterval == null ? ProgressHeartbeatDefaultInterval : progressHeartbeatInterval.Value);
                this.ProgressHeartbeatTimer.Elapsed += this.OnProgressHeartbeatTimerOnElapsed;
            }

            this.timerBasedProgressCheck = timerBasedProgressCheck;
            this.CanAbort = true;
            this.CanReportProgress = true;
            this.StepState = StepState.Initialized;
        }

        #endregion

        #region Public Events

        public event EventHandler<FaultedEventArgs> Faulted = (sender, args) => { };

        public event EventHandler Finished = (sender, args) => { };

        public event EventHandler<ProgressedEventArgs> Progressed = (sender, args) => { };

        #endregion

        #region Public Properties

        public bool CanAbort { get; protected set; }

        public bool CanReportProgress { get; protected set; }

        public StepState StepState { get; private set; }

        #endregion

        #region Public Methods and Operators

        public void Abort()
        {
            if (!this.CanAbort)
            {
                throw new InvalidOperationException("This step is not abortable.");
            }

            if (this.StepState != StepState.Running)
            {
                throw new InvalidOperationException("This step can be only aborted if the current state is Running.");
            }

            try
            {
                this.Logger.LogInformation(string.Format("Aborting step [{0}]...", this.GetType().Name));

                this.StepState = StepState.Aborted;
                this.AbortAction();

                this.Logger.LogInformation(string.Format("Aborted step [{0}].", this.GetType().Name));
            }
            catch (Exception ex)
            {
                this.OnFaulted(ex, FaultedLocation.Abort);
            }
        }

        public double GetProgress()
        {
            return this.progress;
        }

        public TStepInfo Start()
        {
            if (this.StepState != StepState.Initialized)
            {
                throw new InvalidOperationException("This step can be only started if the current state is Initialized.");
            }

            var info = default(TStepInfo);

            try
            {
                this.Logger.LogInformation(string.Format("Starting step [{0}]...", this.GetType().Name));

                this.StepState = StepState.Running;
                info = this.StartAction();

                if (this.timerBasedProgressCheck && this.ProgressHeartbeatTimer != null)
                {
                    this.ProgressHeartbeatTimer.Enabled = true;
                }

                this.Logger.LogInformation(string.Format("Started step [{0}].", this.GetType().Name));
            }
            catch (Exception ex)
            {
                this.OnFaulted(ex, FaultedLocation.Start);
            }

            return info;
        }

        #endregion

        #region Methods

        protected abstract void AbortAction();

        protected abstract void CleanupAction();

        protected virtual void OnFaulted(Exception ex, FaultedLocation fa, int errorCode = 0)
        {
            if (this.StepState != StepState.Running && this.StepState != StepState.Aborted)
            {
                return;
            }

            if (this.ProgressHeartbeatTimer != null)
            {
                this.ProgressHeartbeatTimer.Dispose();
            }

            try
            {
                this.CleanupAction();
            }
            catch (Exception cleanupEx)
            {
                this.Logger.LogWarning(
                    "During transition to the faulted state, the cleanup work also failed. --> " + cleanupEx, ErrorCodes.StepBaseCleanupError);
            }

            this.StepState = StepState.Faulted;
            this.Faulted(this, new FaultedEventArgs(ex, fa, errorCode));
        }

        protected virtual void OnFinished()
        {
            if (this.StepState != StepState.Running)
            {
                return;
            }

            this.StepState = StepState.Finished;
            this.Finished(this, new EventArgs());
        }

        protected virtual void OnProgressed(double p)
        {
            this.Progressed(this, new ProgressedEventArgs(p));
        }

        protected abstract double ProgressHeartbeatAction();

        protected abstract TStepInfo StartAction();

        private void OnProgressHeartbeatTimerOnElapsed(object o, ElapsedEventArgs e)
        {
            try
            {
                var newProgress = this.ProgressHeartbeatAction();

                // clear the accumulated exception count
                this.progressHeartbeatUnexpectedExceptionCount = 0;

                if (newProgress > this.progress)
                {
                    this.progress = newProgress;
                    this.OnProgressed(this.progress);
                }

                if (this.progress < 100)
                {
                    return;
                }

                try
                {
                    this.OnFinished();
                }
                catch (Exception ex)
                {
                    this.OnFaulted(ex, FaultedLocation.OnFinished);
                }

                this.ProgressHeartbeatTimer.Dispose();
            }
            catch (StepFaultException sfe)
            {
                this.OnFaulted(sfe, FaultedLocation.ProgressHeartbeat);
            }
            catch (Exception ex)
            {
                if (++this.progressHeartbeatUnexpectedExceptionCount < ProgressHeartbeatUnexpectedExceptionCountLimit)
                {
                    this.Logger.LogWarning(
                        string.Format(
                            "During progress heartbeat, unexpected error happens, the {0} time. --> {1}", 
                            this.progressHeartbeatUnexpectedExceptionCount, 
                            ex), 
                        ErrorCodes.StepBaseProgressHeartbeatError);
                    return;
                }

                this.OnFaulted(ex, FaultedLocation.ProgressHeartbeat);
            }
        }

        #endregion
    }
}
